#!/usr/bin/python
#
# (c) 2011-2016 Dennis Kaarsemaker <dennis@kaarsemaker.net>
# see COPYING for license details

try:
    import ConfigParser
except ImportError:
    import configparser as ConfigParser
import hpilo
import optparse
import os
import re
import subprocess
import sys
import time

def main():
    usage = """%prog [options] init
%prog [options] sign hostname [hostname ...]"""
    p = optparse.OptionParser(usage=usage)
    p.add_option("-l", "--login", dest="login", default=None,
                 help="Username to access the iLO")
    p.add_option("-p", "--password", dest="password", default=None,
                 help="Password to access the iLO")
    p.add_option("-c", "--config", dest="config", default="~/.ilo.conf",
                 help="File containing authentication and config details", metavar="FILE")
    p.add_option("-t", "--timeout", dest="timeout", type="int", default=60,
                 help="Timeout for iLO connections")
    p.add_option("-P", "--protocol", dest="protocol", choices=("http","raw"), default=None,
                 help="Use the specified protocol instead of autodetecting")
    p.add_option("-d", "--debug", dest="debug", action="count", default=0,
                 help="Output debug information, repeat to see all XML data")
    p.add_option("-o", "--port", dest="port", type="int", default=443,
                 help="SSL port to connect to")
    p.add_option("-s", "--openssl", dest="openssl_bin", default="/usr/bin/openssl",
                 help="Path to the openssl binary")
    p.add_option("-u", "--upgrade", dest="upgrade", action="store_true", default=False,
                 help="Upgrade iLO firmware that is too old automatically")

    opts, args = p.parse_args()

    if not args or args[0] not in ('init','sign'):
        p.print_help()
        p.exit(1)

    if not os.path.exists(os.path.expanduser(opts.config)):
        p.error("Configuration file '%s' does not exist" % opts.config)

    config = ConfigParser.ConfigParser()
    config.read(os.path.expanduser(opts.config))
    if not config.has_option('ca', 'path'):
        p.error("No CA path specified in the config")
    ca_path = os.path.expanduser(config.get('ca', 'path'))
    os.environ['CA_PATH'] = ca_path
    os.environ['SUBJECTALTNAME'] = ''
    csr_params = {}
    for key in ('country', 'state', 'locality', 'organization', 'organizational_unit'):
        if config.has_option('ca', key):
            csr_params[key] = config.get('ca', key)

    openssl = OpenSSL(opts.openssl_bin, ca_path, csr_params)

    if args[0] == 'init':
        if len(args) > 2:
            p.error("Too many arguments")
        openssl.init_ca()
        sys.exit(0)

    # Do we have login information
    login = None
    password = None
    if config.has_option('ilo', 'login'):
        login = config.get('ilo', 'login')
    if config.has_option('ilo', 'password'):
        password = config.get('ilo', 'password')
    if opts.login:
        login = opts.login
    if opts.password:
        password = opts.password
    if not login or not password:
        p.error("No login details provided")

    todo = args[1:]
    for host in args[1:]:
        if re.match(r'\d+(\.\d+){3}', host):
            p.error("hpilo_ca only accepts fqdns, not IP addresses")
        if '.' not in host:
            p.error("hpilo_ca only accepts fqdns, not hostnames")
    while todo:
        todo = sign_certificates(todo, openssl, login, password, opts)
        if todo:
            print("Waiting a minute before trying %d hosts again" % len(todo))
            what = "\|/-"
            for i in range(60):
                sys.stdout.write("\r%s %-2d" % (what[i%4], 60-i))
                sys.stdout.flush()
                time.sleep(1)
            sys.stdout.write("\r    \r")
            sys.stdout.flush()

def sign_certificates(hosts, openssl, login, password, opts):
    todo = []
    for hostname in hosts:
        if openssl.signed(hostname):
            print("Certificate already signed, skipping")
            continue

        ilo = hpilo.Ilo(hostname, login, password, opts.timeout, opts.port)
        ilo.debug = opts.debug
        if opts.protocol == 'http':
            ilo.protocol = hpilo.ILO_HTTP
        elif opts.protocol == 'raw':
            ilo.protocol = hpilo.ILO_RAW

        print("(1/5) Checking certificate config of %s" % hostname)
        fw_version = ilo.get_fw_version()
        network_settings = ilo.get_network_settings()
        if fw_version['management_processor'].lower() == 'ilo2':
            version = fw_version['firmware_version']
            if version < '2.06':
                print("iLO2 firmware version %s is too old" % version)
                if not opts.upgrade:
                    print("Please upgrade firmware to 2.06 or newer")
                    continue
                print("Upgrading iLO firmware")
                ilo.update_rib_firmware(version='latest', progress=print_progress)
                todo.append(hostname)
                continue
            cn = ilo.get_cert_subject_info()['csr_subject_common_name']
            if '.' not in cn:
                ilo.cert_fqdn(use_fqdn=True)
                cn = ilo.get_cert_subject_info()['csr_subject_common_name']

        hn, dn = hostname.split('.', 1)
        if network_settings['dns_name'] != hn or dn and network_settings['domain_name'] != dn:
           print("Hostname misconfigured on the ilo, fixing")
           ilo.mod_network_settings(dns_name=hn, domain_name=dn)
           todo.append(hostname)
           continue

        print("(2/5) Retrieving certificate signing request")
        if fw_version['management_processor'].lower() == 'ilo2':
            csr = ilo.certificate_signing_request()
        else:
            params = openssl.csr_params.copy()
            cn = '%s.%s' % (network_settings['dns_name'], network_settings['domain_name'])
            params['common_name'] = cn
            try:
                csr = ilo.certificate_signing_request(**params)
            except hpilo.IloGeneratingCSR:
                print("Certificate request is being generated, trying again later")
                todo.append(hostname)
                continue
        if not csr:
            print("Received an empty CSR")
            continue

        print("(3/5) Signing certificate")
        cert = openssl.sign(network_settings, csr)

        print("(4/5) Uploading certificate")
        ilo.import_certificate(cert)

        print("(5/5) Resetting iLO")
        ilo.reset_rib()
    return todo

class OpenSSL(object):
    def __init__(self, openssl_bin, ca_path, csr_params):
        self.openssl_bin = openssl_bin
        self.ca_path = ca_path
        self.openssl_cnf = os.path.join(ca_path, 'openssl.cnf')
        self.csr_params = csr_params

    def call(self, *args, **envvars):
        env = os.environ.copy()
        env.update(envvars)
        args = list(args)
        if args[0] in ('req', 'ca'):
            args = args[:1] + ['-config', self.openssl_cnf] + args[1:]
        return subprocess.call([self.openssl_bin] + args, env=env)

    def signed(self, hostname):
        crt_path = os.path.join(self.ca_path, 'certs', hostname + '.crt')
        return os.path.exists(crt_path)

    def sign(self, network_settings, csr):
        hostname = network_settings['dns_name']
        fqdn = '%s.%s' % (network_settings['dns_name'], network_settings['domain_name'])
        ip = network_settings['ip_address']
        altnames = 'DNS:%s,DNS:%s,IP:%s' % (fqdn, hostname, ip)

        csr_path = os.path.join(self.ca_path, 'certs', hostname + '.csr')
        crt_path = os.path.join(self.ca_path, 'certs', hostname + '.crt')
        fd = open(csr_path, 'w')
        fd.write(csr)
        fd.close()
        self.call('ca', '-batch', '-in', csr_path, '-out', crt_path, SUBJECTALTNAME=altnames)
        fd = open(crt_path)
        cert = fd.read()
        fd.close()
        cert = cert[cert.find('-----BEGIN'):]
        return cert

    def init_ca(self):
        if not os.path.exists(self.ca_path):
            os.makedirs(self.ca_path)
        if not os.path.exists(self.openssl_cnf):
            open(self.openssl_cnf, 'w').write(self.default_openssl_cnf)
        for dir in ('ca', 'certs', 'crl', 'archive'):
            if not os.path.exists(os.path.join(self.ca_path, dir)):
                os.mkdir(os.path.join(self.ca_path, dir))
        if not os.path.exists(os.path.join(self.ca_path, 'archive', 'index')):
            open(os.path.join(self.ca_path, 'archive', 'index'),'w').close()
        if not os.path.exists(os.path.join(self.ca_path, 'archive', 'serial')):
            fd = open(os.path.join(self.ca_path, 'archive', 'serial'),'w')
            fd.write("00\n")
            fd.close()
        if not os.path.exists(os.path.join(self.ca_path, 'ca', 'hpilo_ca.key')):
            self.call('genrsa', '-out', os.path.join(self.ca_path, 'ca', 'hpilo_ca.key'), '2048')
        if not os.path.exists(os.path.join(self.ca_path, 'ca', 'hpilo_ca.crt')):
            self.call('req', '-new', '-x509', '-days', '7300', # 20 years should be enough for everyone :-)
                      '-key', os.path.join(self.ca_path, 'ca', 'hpilo_ca.key'),
                      '-out', os.path.join(self.ca_path, 'ca', 'hpilo_ca.crt'))

    default_openssl_cnf = """default_ca = hpilo_ca

[hpilo_ca]
dir              = $ENV::CA_PATH
certs            = $dir/certs
crl_dir          = $dir/crl
private_key      = $dir/ca/hpilo_ca.key
certificate      = $dir/ca/hpilo_ca.crt
database         = $dir/archive/index
new_certs_dir    = $dir/archive
serial           = $dir/archive/serial
crlnumber        = $dir/crl/crlnumber
crl              = $dir/crl/crl.pem
RANDFILE         = $dir/.rand
x509_extensions  = usr_cert
name_opt         = ca_default
cert_opt         = ca_default
default_days     = 1825 # 5 years
default_crl_days = 30
default_md       = sha256
preserve         = no
policy           = req_policy

[req_policy]
countryName            = supplied
stateOrProvinceName    = supplied
localityName           = supplied
organizationName       = supplied
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

[req]
dir                = $ENV::CA_PATH
default_bits       = 2048
default_md         = sha256
default_keyfile    = $dir/ca/hpilo_ca.key
distinguished_name = req_distinguished_name
x509_extensions    = ca_cert # The extentions to add to the self signed cert
string_mask        = MASK:0x2002

[req_distinguished_name]
countryName                 = Country Name (2 letter code)
countryName_default         = NL
countryName_min             = 2
countryName_max             = 2
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = Flevoland
localityName                = Locality Name (eg, city)
localityName_default        = Lelystad
0.organizationName          = Organization Name (eg, company)
0.organizationName_default  = Kaarsemaker.net
commonName                  = Common Name (eg, your name or your server's hostname)
commonName_max              = 64
commonName_default          = hpilo_ca

[usr_cert]
basicConstraints       = CA:FALSE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
nsComment              = "Certificate generated by iLO CA"
subjectAltName         = $ENV::SUBJECTALTNAME

[ca_cert]
basicConstraints       = CA:true
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer:always
nsComment              = "Certificate generated by iLO CA"
"""

def print_progress(text):
    if text.startswith('\r'):
        sys.stdout.write(text)
        sys.stdout.flush()
    else:
        print(text)

if __name__ == '__main__':
    main()
